/* eslint-disable @babel/no-invalid-this */
const visit = require(`unist-util-visit`)
const isRelativeUrl = require(`is-relative-url`)
const fsExtra = require(`fs-extra`)
const path = require(`path`)
const _ = require(`lodash`)
const cheerio = require(`cheerio`)
const imageSize = require(`probe-image-size`)

const DEPLOY_DIR = `public`

const invalidDestinationDirMessage = dir =>
  `[gatsby-remark-copy-linked-files You have supplied an invalid destination directory. The destination directory must be a child but was: ${dir}`

// dest must be a child
const destinationIsValid = dest => !path.relative(`./`, dest).startsWith(`..`)

const validateDestinationDir = dir => {
  if (typeof dir === `undefined`) {
    return true
  } else if (typeof dir === `string`) {
    // need to pass dummy data for validation to work
    return destinationIsValid(`${dir}/n/h/a`)
  } else if (_.isFunction(dir)) {
    // need to pass dummy data for validation to work
    return destinationIsValid(
      `${dir({ name: `n`, hash: `h`, absolutePath: `a` })}`
    )
  } else {
    return false
  }
}

const defaultDestination = linkNode =>
  `${linkNode.internal.contentDigest}/${linkNode.name}.${linkNode.extension}`

const getDestination = (linkNode, dir) => {
  if (_.isFunction(dir)) {
    return `${dir({
      name: linkNode.name,
      hash: linkNode.internal.contentDigest,
      absolutePath: linkNode.absolutePath,
    })}.${linkNode.extension}`
  } else if (_.isString(dir)) {
    return `${dir}/${defaultDestination(linkNode)}`
  } else {
    return defaultDestination(linkNode)
  }
}

const newPath = (linkNode, options) => {
  const { destinationDir } = options
  const destination = getDestination(linkNode, destinationDir)
  const paths = [process.cwd(), DEPLOY_DIR, destination]
  return path.posix.join(...paths)
}

const newLinkURL = (linkNode, options, pathPrefix) => {
  const { destinationDir } = options
  const destination = getDestination(linkNode, destinationDir)
  const startsWithSlash = destination.startsWith(`/`)
  return `${pathPrefix ? pathPrefix : ``}${
    startsWithSlash ? `` : `/`
  }${destination}`
}

function toArray(buf) {
  const arr = new Array(buf.length)

  for (let i = 0; i < buf.length; i++) {
    arr[i] = buf[i]
  }

  return arr
}

module.exports = (
  { files, markdownNode, markdownAST, pathPrefix, getNode },
  pluginOptions = {}
) => {
  const defaults = {
    ignoreFileExtensions: [`png`, `jpg`, `jpeg`, `bmp`, `tiff`],
  }
  const { destinationDir } = pluginOptions
  if (!validateDestinationDir(destinationDir))
    return Promise.reject(invalidDestinationDirMessage(destinationDir))

  const options = _.defaults({}, pluginOptions, defaults)

  const filesToCopy = new Map()
  // Copy linked files to the destination directory and modify the AST to point
  // to new location of the files.
  const visitor = link => {
    if (isRelativeUrl(link.url) && getNode(markdownNode.parent).dir) {
      const linkPath = path.posix.join(
        getNode(markdownNode.parent).dir,
        link.url
      )
      const linkNode = _.find(files, file => {
        if (file && file.absolutePath) {
          return file.absolutePath === linkPath
        }
        return null
      })
      if (linkNode && linkNode.absolutePath) {
        const newFilePath = newPath(linkNode, options)

        // Prevent unneeded copying
        if (linkPath === newFilePath) return

        const linkURL = newLinkURL(linkNode, options, pathPrefix)
        link.url = linkURL
        filesToCopy.set(linkPath, newFilePath)
      }
    }
  }

  // Takes a node and generates the needed images and then returns
  // the needed HTML replacement for the image
  const generateImagesAndUpdateNode = function (image, node) {
    const imagePath = path.posix.join(
      getNode(markdownNode.parent).dir,
      image.attr(`src`)
    )
    const imageNode = _.find(files, file => {
      if (file && file.absolutePath) {
        return file.absolutePath === imagePath
      }
      return null
    })
    if (!imageNode || !imageNode.absolutePath) {
      return
    }

    const initialImageSrc = image.attr(`src`)
    // The link object will be modified to the new location so we'll
    // use that data to update our ref
    const link = { url: image.attr(`src`) }
    visitor(link)
    node.value = node.value.replace(
      new RegExp(image.attr(`src`), `g`),
      link.url
    )

    let dimensions

    if (!image.attr(`width`) || !image.attr(`height`)) {
      dimensions = imageSize.sync(
        toArray(fsExtra.readFileSync(imageNode.absolutePath))
      )
    }

    // Generate default alt tag
    const srcSplit = initialImageSrc.split(`/`)
    const fileName = srcSplit[srcSplit.length - 1]
    const fileNameNoExt = fileName.replace(/\.[^/.]+$/, ``)
    const defaultAlt = fileNameNoExt.replace(/[^A-Z0-9]/gi, ` `)

    image.attr(`alt`, image.attr(`alt`) ? image.attr(`alt`) : defaultAlt)
    image.attr(
      `width`,
      image.attr(`width`) ? image.attr(`width`) : dimensions.width
    )
    image.attr(
      `height`,
      image.attr(`height`) ? image.attr(`height`) : dimensions.height
    )
  }

  visit(markdownAST, `link`, link => {
    const ext = link.url.split(`.`).pop()
    if (options.ignoreFileExtensions.includes(ext)) {
      return
    }

    visitor(link)
  })

  visit(markdownAST, `definition`, definition => {
    const ext = definition.url.split(`.`).pop()
    if (options.ignoreFileExtensions.includes(ext)) {
      return
    }

    visitor(definition)
  })

  // This will only work for markdown img tags
  visit(markdownAST, `image`, image => {
    const ext = image.url.split(`.`).pop()
    if (options.ignoreFileExtensions.includes(ext)) {
      return
    }

    // Just make sure the parent node has dir
    if (markdownNode.parent && !getNode(markdownNode.parent).dir) {
      return
    }

    const imagePath = path.posix.join(
      getNode(markdownNode.parent).dir,
      image.url
    )
    const imageNode = _.find(files, file => {
      if (file && file.absolutePath) {
        return file.absolutePath === imagePath
      }
      return false
    })

    if (imageNode) {
      visitor(image)
    }
  })

  // For each HTML Node
  visit(markdownAST, [`html`, `jsx`], node => {
    const $ = cheerio.load(node.value)

    function processUrl({ url, isRequired }) {
      try {
        const ext = url.split(`.`).pop()
        if (!options.ignoreFileExtensions.includes(ext) || isRequired) {
          // The link object will be modified to the new location so we'll
          // use that data to update our ref
          const link = { url }
          visitor(link)
          node.value = node.value.replace(new RegExp(url, `g`), link.url)
        }
      } catch (err) {
        // Ignore
      }
    }

    // extracts all elements that have the provided url attribute
    function extractUrlAttributeAndElement(selection, attribute) {
      return (
        selection
          // extract the elements that have the attribute
          .map(function () {
            const element = $(this)
            const url = $(this).attr(attribute)
            if (url && isRelativeUrl(url)) {
              return { url, element }
            }
            return undefined
          })
          // cheerio object -> array
          .toArray()
          // filter out empty or undefined values
          .filter(Boolean)
      )
    }

    // Handle Images
    extractUrlAttributeAndElement($(`img[src]`), `src`).forEach(
      ({ url, element }) => {
        try {
          const ext = url.split(`.`).pop()
          if (!options.ignoreFileExtensions.includes(ext)) {
            generateImagesAndUpdateNode(element, node)
          }
        } catch (err) {
          // Ignore
        }
      }
    )

    // Handle video tags.
    extractUrlAttributeAndElement(
      $(`video source[src], video[src]`),
      `src`
    ).forEach(processUrl)

    // Handle video poster.
    extractUrlAttributeAndElement($(`video[poster]`), `poster`).forEach(
      extractedUrlAttributeAndElement =>
        processUrl({ ...extractedUrlAttributeAndElement, isRequired: true })
    )

    // Handle audio tags.
    extractUrlAttributeAndElement(
      $(`audio source[src], audio[src]`),
      `src`
    ).forEach(processUrl)

    // Handle flash embed tags.
    extractUrlAttributeAndElement($(`object param[value]`), `value`).forEach(
      processUrl
    )

    // Handle a tags.
    extractUrlAttributeAndElement($(`a[href]`), `href`).forEach(processUrl)

    return
  })

  return Promise.all(
    Array.from(filesToCopy, async ([linkPath, newFilePath]) => {
      // Don't copy anything if the file already exists at the location.
      if (!fsExtra.existsSync(newFilePath)) {
        try {
          await fsExtra.ensureDir(path.dirname(newFilePath))
          await fsExtra.copy(linkPath, newFilePath)
        } catch (err) {
          console.error(`error copying file`, err)
        }
      }
    })
  )
}
